Skip to content

feat(auth-ui): reusable AuthPanel (React + vanilla) + alias-publish - #458

Merged
izzywdev merged 8 commits into
masterfrom
feat/auth-ui-package
Jul 29, 2026
Merged

feat(auth-ui): reusable AuthPanel (React + vanilla) + alias-publish#458
izzywdev merged 8 commits into
masterfrom
feat/auth-ui-package

Conversation

@izzywdev

Copy link
Copy Markdown
Owner

Why

FuzeFront had no reusable sign-in/sign-up componentfrontend/src/pages/LoginPage.tsx is a zero-prop route page welded to LanguageContext, useCurrentUser, an axios singleton, and window.location. FuzePicker's extension widget re-implemented the same auth form by hand (with a credential-leak bug). This package extracts the capability once, so both the app and the extension consume one component.

What

New @fuzefront/auth-ui with two consumption modes from one design:

  • React AuthPanel (src/components/AuthPanel.tsx) — injection-seam props, none of LoginPage's hidden coupling:
    • transport: AuthTransport — host supplies login/signup/getAuthMethods/optional startSocial, so the same component runs against the app's axios client OR an extension's chrome.runtime bridge.
    • variant: 'compact' | 'full' (full wraps in CenteredCard), mode, onAuthenticated, onMfaRequired, social, labels (all strings injected — i18n without useLanguage).
    • Behaviour parity with LoginPage: password sign-in/sign-up, mode toggle, per-action pending labels, error + mfa_required handling, optional Google button (only when startSocial present and methods advertise it; Google mark keeps the ds-conformance-allow exemption).
    • Types imported from @fuzefront/security-client (SessionResult, AuthenticatedSession, MfaRequiredChallenge, AuthMethods, …) — not redefined.
  • Vanilla dist/auth-ui.vanilla.js — a zero-dependency IIFE (esbuild.vanilla.mjs) exposing window.FuzeFrontAuthUI.mount(container, opts) / .unmount(), same markup + same token CSS classes, no React in the bundle. This is what the no-bundler Chrome extension embeds (social defaults off there).
  • ./styles.css — component classes, all values via fuse-seam DS tokens.
  • exports map: . (React), ./vanilla (IIFE), ./styles.css. publishConfig → GitHub Packages, restricted.

Two alias-publish workflows (mirroring security-packages-publish.yml, owner-gated izzywdev, idempotent, no git push/tag):

  • auth-ui-packages-publish.yml: @fuzefront/auth-ui → @izzywdev/fuzefront-auth-ui
  • design-system-packages-publish.yml: @fuzefront/design-system → @izzywdev/fuzefront-design-system (unblocks the DS, which had never published — its canonical workflow is gated on the fuzefront org that doesn't own the repo).

Verified

  • Vanilla bundle is React-free at source: src/vanilla/{entry,mount}.ts import only a type + DEFAULT_AUTH_LABELS; no react/react-dom import anywhere under src/vanilla/. esbuild can only bundle what's imported, so the IIFE cannot contain React.
  • Package shape audited: exports, build (vite + tsc --emitDeclarationOnly + esbuild vanilla), publishConfig, peerDeps (react18 + @fuzefront/design-system) all correct.
  • Both workflows are valid, owner-gated, path-scoped, idempotent.

NOT verified locally (CI is the check of record)

npm run build / type-check / test were not run to green on the local Windows host: the FuzeFront monorepo .npmrc pins os=linux (for CI Docker), so the installed esbuild native binary is @esbuild/aix-ppc64, not @esbuild/win32-x64, and the local build errors out on platform mismatch. This is the known repo gotcha, not a code issue — CI (ubuntu-latest) builds/tests authoritatively via the standard gates. Please confirm the gates go green before merge. Tests are written (AuthPanel.test.tsx, vanilla/mount.test.ts) but their pass/fail is CI's to report.

Follow-ups (separate PRs)

  • Refactor LoginPage.tsx to consume <AuthPanel> (removes the duplication).
  • FuzePicker embeds the vanilla build as an extension-origin iframe (also fixes the host-credential autofill leak).

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

fuzeone-bot Bot and others added 6 commits July 30, 2026 00:13
Safety checkpoint before implementation: package.json, tsconfig*.json,
vite.config.ts, README, stub src/index.ts. Mirrors packages/identity-ui's
build setup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session-Id: 548a2d55-e1cd-4b5e-bfc4-b6e102bf4695
Types derived from @fuzefront/security-client's SessionResult union rather
than hand-redefined.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session-Id: 548a2d55-e1cd-4b5e-bfc4-b6e102bf4695
Sign-in/sign-up form with mode toggle, per-action pending labels, error/mfa
notice, and optional Google button — design-system-first (Button, Input,
Alert, SeamDivider, CenteredCard only), transport-injected (no useLanguage/
useCurrentUser/window.location/asset imports).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session-Id: 548a2d55-e1cd-4b5e-bfc4-b6e102bf4695
window.FuzeFrontAuthUI.mount(container, opts) reimplements the same
behavior/CSS classes as plain DOM, no React/React-DOM in the bundle. social
defaults OFF for the vanilla entry point (React AuthPanel defaults social on).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session-Id: 548a2d55-e1cd-4b5e-bfc4-b6e102bf4695
Covers sign-in/signup submit, mode toggle, pending labels, mfa_required
notice/callback, error alert, and Google button visibility gating
(present only with startSocial AND methods.social advertising it; vanilla
defaults social off even when startSocial is supplied).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session-Id: 548a2d55-e1cd-4b5e-bfc4-b6e102bf4695
Mirrors .github/workflows/security-packages-publish.yml: idempotent,
owner-gated (izzywdev), no git push/tag. Adds packages/auth-ui to the
root workspaces list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session-Id: 548a2d55-e1cd-4b5e-bfc4-b6e102bf4695
@github-actions
github-actions Bot enabled auto-merge (squash) July 29, 2026 21:37
@github-actions

Copy link
Copy Markdown
Contributor

Automated code review (gate-code-review)

Credit balance is too low

Report-only — this check never blocks merge.

CI runs `npm ci`, which requires package-lock.json to be in sync. The new
package's devDeps (esbuild, vite, vitest, jsdom) were never added to the root
lock, so `npm ci` failed at install — cascading to every downstream gate
(frontend-build, unit tests, audit). Regenerated with `npm install
--package-lock-only` (metadata only; no native-binary fetch, so unaffected by
the os=linux .npmrc pin).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session-Id: 548a2d55-e1cd-4b5e-bfc4-b6e102bf4695
@github-actions

Copy link
Copy Markdown
Contributor

Automated code review (gate-code-review)

Credit balance is too low

Report-only — this check never blocks merge.

Collateral from this PR's lockfile update: regenerating package-lock.json
deduped @types/node, so chat-service now resolves 24.x, where `Dirent` became
generic. The annotation `Awaited<ReturnType<typeof fs.readdir>>` selected the
Buffer overload, typing `entry.name` as Buffer and breaking `.startsWith` /
`.toLowerCase` (index-docs.ts type-check, TS2322/2345/2367/2339).

Drop the fragile annotation and infer the element type from the default (utf8)
readdir call → Dirent<string>. Version-robust (compiles under @types/node 18
and 24) and behavior-identical. Verified locally: the index-docs Dirent errors
are gone (remaining local tsc noise is this worktree's partial install, absent
in CI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session-Id: 548a2d55-e1cd-4b5e-bfc4-b6e102bf4695
@github-actions

Copy link
Copy Markdown
Contributor

Automated code review (gate-code-review)

Credit balance is too low

Report-only — this check never blocks merge.

@izzywdev
izzywdev merged commit 0fea568 into master Jul 29, 2026
50 checks passed
@izzywdev
izzywdev deleted the feat/auth-ui-package branch July 29, 2026 22:27
izzywdev pushed a commit that referenced this pull request Jul 29, 2026
…r-menu-clplz8

Third merge from master. Two conflicts, both resolved in master's favour, plus
one latent defect of my own that the merge brought into view.

**`services/chat-service/src/rag/index-docs.ts`** — master fixed the SAME
`Awaited<ReturnType<typeof fs.readdir>>` bug I had just fixed, with the same
root-cause diagnosis (ReturnType resolves to readdir's Buffer overload under
@types/node 24) and an equivalent remedy: infer from the call rather than
annotate. Took master's version byte-for-byte and dropped my now-unneeded
`type Dirent` import. It is their file and their fix; divergence here buys
nothing.

**`package-lock.json`** — master removed the stale nested
`services/chat-service/node_modules/@types/node@18.19.0`, which is exactly the
lockfile reconciliation my previous commit flagged as needed but out of scope.
Resolved by taking master's lock wholesale and re-running `npm install` to
re-derive this branch's `services/notification-service` subtree, rather than
hand-editing lock JSON. Net effect: there is now exactly ONE `@types/node` in the
tree, the root `24.13.3` — every nested Node-18 copy is gone, so notification-
service resolves the version its manifest asks for and the class of failure that
broke CI cannot recur silently in these workspaces.

**`frontend/e2e/post-prod/live-smoke.spec.ts`** — not a conflict; a spec my
earlier sweep missed because it lives under `e2e/post-prod/` rather than
`tests/`. It seeded the bare `authToken` key before app boot. That still boots
today, but only because the account vault's one-time legacy migration sweeps it
— the same "upgrade path used as a write channel" trap already fixed in
WhiteLabelLoginCard. This one matters more: it is the POST-PRODUCTION synthetic
that verifies the live deployment, so it would have gone quietly red against prod
the day that migration is retired. Now routed through the shared
`seedMockSession` helper, whose docstring is generalised since it now seeds a
real prod token as well as fixtures.

Verified after the merge:
- frontend `tsc --noEmit` clean; vitest 136/136 across 19 files
- backend 91/91 (app-installations, notification-proxy, apps, provisioning,
  root-org-admin) against real Postgres, `--runInBand`
- chat-service 131 passed / 2 skipped across 24 suites; `tsc` clean
- notification-service `tsc` clean, 35/35, now resolving root 24.13.3
- master's new `@fuzefront/auth-ui` 17/17
- no duplicate migration ordinals: 014, 015, 016, 017
- `packages/auth-ui` touches no storage, so #458 introduces no bare-token write

Note for anyone running the backend suites locally: master's Permit ReBAC work
makes `PERMIT_API_KEY` mandatory at import time, so they need
`PERMIT_API_KEY=ci-no-real-permit-calls` (what CI passes) or every suite fails in
setup before a single test runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014KynhzG6wKxUnR8KWwK8un
izzywdev pushed a commit that referenced this pull request Jul 29, 2026
Adds the security-client -> auth-ui build chain the frontend type-check
now depends on (tsconfig resolves auth-ui to its dist/index.d.ts), plus
auth-ui's own type-check + vitest run — neither package had any CI
coverage before this (PR #458 wired the publish workflow but not ci.yml).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session-Id: 548a2d55-e1cd-4b5e-bfc4-b6e102bf4695
izzywdev pushed a commit that referenced this pull request Jul 29, 2026
Pre-existing bug in PR #458, uncaught until now because auth-ui had no
type-check step in ci.yml (only added in this PR). input.autocomplete
is typed as the strict DOM `AutoFill` token union; a plain `string`
param can't assign to it. Use setAttribute instead, which accepts any
string and the property getter reflects regardless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session-Id: 548a2d55-e1cd-4b5e-bfc4-b6e102bf4695
izzywdev pushed a commit that referenced this pull request Aug 2, 2026
…eed with the server's real default

"Sign-in with Google seems to disappear. It doesn't show in some cases."

Root cause: three places render the Google button gated on `authMethods.social.includes('google')`, and all three fall back to a LOCAL guess of that value while `GET /v1/security/methods` is in flight, or if it fails outright. That guess was `social: []`.

The server's actual default (routes/security.ts) is the opposite: Google is ENABLED unless `SECURITY_SOCIAL_GOOGLE` is explicitly set to `'false'`, and no deploy env sets it. So the fallback didn't describe "we don't know yet" — it confidently described a DIFFERENT, wrong state. Every time that fetch was slow, raced a fast unmount, or errored, a legitimately available Google button silently vanished for the duration of that render, with nothing resembling an error to explain it. That is exactly "some cases" — it depends on network timing, not on any deliberate server config.

Fixed in all three places that duplicate this fallback:
  - frontend/src/pages/LoginPage.tsx (FALLBACK_METHODS)
  - packages/auth-ui/src/components/AuthPanel.tsx (FALLBACK_METHODS) — the
    new reusable panel from #458, same bug, same shape
  - packages/auth-ui/src/vanilla/mount.ts (FALLBACK_METHODS) — the
    non-React embed; its SEPARATE `social` opt-in default (OFF, intentional,
    documented) is untouched, only the "what does the server actually do"
    guess changes

Each fallback now sets `social: ['google']`, matching the security service's
real default. If that default is ever changed, the fallback must be updated
to match — it describes reality, not a guess, and a stale one would fail
the exact same way in the opposite direction.

Tests: one new case per surface (LoginPage, AuthPanel, vanilla mount) —
GET /methods rejecting must still render/show the Google button, not hide
it. Existing "no Google when methods explicitly return social: []" cases
are untouched (still resolve, still hide correctly) — only the FAILURE
path changes.

Verified: auth-ui package 19/19 tests pass; frontend suite unchanged
against master except the new test (66 -> 68, +2 across LoginPage and
this PR's earlier oidc-google-signin.test.ts addition).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhfKNASYBD9aS3Wu6RUPfZ
izzywdev added a commit that referenced this pull request Aug 2, 2026
…d-trip (#459)

Google button: three surfaces (LoginPage, the shared AuthPanel from #458,
and the vanilla mount) rendered the button gated on a local fallback used
while GET /methods is loading or if it fails. That fallback said
social: [], but the server's real default is Google ENABLED unless
SECURITY_SOCIAL_GOOGLE is explicitly 'false' (no deploy env sets it). Any
slow fetch, unmount race, or outright failure silently hid an available
button. Fixed all three fallbacks to match the server's real default;
added a failing-fetch test per surface.

userinfo round-trip: handleCallback() now reads claims from the ID token
(tokenSet.claims()) instead of making a second HTTP call to userinfo.
include_claims_in_id_token: true plus the openid/email/profile scope
mappings already put every field syncUserToDatabase reads into the token
returned by the exchange, verified by the same signature/issuer/audience
checks. Falls back to a real userinfo call when email is absent, since
email is the key user-sync matches accounts on. Removes one of six
sequential provider round-trips from every password login and social
callback.

Verified via a clean git worktree of origin/master (not stash): backend/
security 31 suites identical pass/fail set, frontend 12 suites identical
set plus the new test, packages/auth-ui 19/19, oidc-google-signin.test.ts
45/45.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhfKNASYBD9aS3Wu6RUPfZ
izzywdev added a commit that referenced this pull request Aug 2, 2026
…465)

* refactor(frontend): LoginPage consumes @fuzefront/auth-ui AuthPanel

WIP checkpoint: LoginPage.tsx is now a thin adapter around AuthPanel
(variant="compact" — .auth-form already supplies the card chrome). The
AuthTransport wraps authAPI and reproduces the prior error-message
taxonomy so AuthPanel's generic Error.message rendering shows identical
wording. Page-load social-callback handling stays in LoginPage (page
routing, not a panel concern). Config wiring (tsconfig/vite/vitest
aliases, CI build steps) and test updates follow in subsequent commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session-Id: 548a2d55-e1cd-4b5e-bfc4-b6e102bf4695

* build(frontend): wire @fuzefront/auth-ui as an in-repo workspace package

Resolve @fuzefront/auth-ui from SOURCE for vite/vitest bundling+tests
(mirroring identity-ui), and to its built dist/index.d.ts for the
frontend type-check (mirroring identity-ui/chat-ui/i18n) — it's TSX and
imports @fuzefront/design-system, so type-checking its source under the
frontend's own tsc would pull a second @types/react/csstype copy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session-Id: 548a2d55-e1cd-4b5e-bfc4-b6e102bf4695

* ci(frontend): build + verify @fuzefront/auth-ui in the CI pipeline

Adds the security-client -> auth-ui build chain the frontend type-check
now depends on (tsconfig resolves auth-ui to its dist/index.d.ts), plus
auth-ui's own type-check + vitest run — neither package had any CI
coverage before this (PR #458 wired the publish workflow but not ci.yml).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session-Id: 548a2d55-e1cd-4b5e-bfc4-b6e102bf4695

* test(frontend): update LoginPage tests for the AuthPanel adapter

- Removes LoginPage.signup-validation.test.tsx: it asserted a
  confirm-password field, password-policy-gated submit, and inline
  email-availability check, none of which @fuzefront/auth-ui's AuthPanel
  (v0.1.0) implements. Reintroducing them as one-off LoginPage markup
  would refork logic AuthPanel is meant to own; tracked as a fast-follow
  against the package instead.
- Adds LoginPage.auth-panel-adapter.test.tsx covering the new adapter
  boundary: signup submits through the transport and redirects, a
  rejected signup surfaces the mapped friendly error (not the raw axios
  message), and a social-callback mfa_required result shows the
  page-level notice.

LoginPage.signup-route.test.tsx, LoginPage.google-signin.test.tsx, and
LoginPage.submit-resilience.test.tsx are unchanged and expected to keep
passing — mode-detection, the Google/credentials paths, and the login
error taxonomy (reproduced in LoginPage.tsx's mapAuthError, since
AuthPanel just renders Error.message as-is) are all preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session-Id: 548a2d55-e1cd-4b5e-bfc4-b6e102bf4695

* fix(auth-ui): satisfy AutoFill's strict type in the vanilla mount

Pre-existing bug in PR #458, uncaught until now because auth-ui had no
type-check step in ci.yml (only added in this PR). input.autocomplete
is typed as the strict DOM `AutoFill` token union; a plain `string`
param can't assign to it. Use setAttribute instead, which accepts any
string and the property getter reflects regardless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session-Id: 548a2d55-e1cd-4b5e-bfc4-b6e102bf4695

* fix(frontend): drop unused React import from LoginPage.tsx

jsx: "react-jsx" means the React namespace import isn't needed, and
noUnusedLocals (frontend/tsconfig.json) flags it as TS6133.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session-Id: 548a2d55-e1cd-4b5e-bfc4-b6e102bf4695

---------

Co-authored-by: fuzeone-bot[bot] <fuzeone-bot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant